作為一個開發者,遍歷一個字串,一個物件,一個陣列,確認裡面的屬性與值,是再常見不過的場景。
在 ES 6 以前,這些場景和 for
相關的語法總脫不了關係,比如
let str = "string"
let newStr = "";
for(let i = 0; i < str.length; i++) newStr += str[i] + ","
console.log(newStr);//"s,t,r,i,n,g,"
但為了應對更全面的場景,ES 6 推出了一些新的資料結構如:
Object
,主要處理鍵值對類型的資料,差異在 Map
的鍵可以為任意型別,而 Object
的鍵只能為字串或 Symbol
。同時,Map
的鍵插入操作序會影響其中的儲存順序,Object
則是依自己的一套邏輯來排序鍵。此外,Map
提供了一些方便的屬性,如 size
等可以直接存取鍵數量的屬性。
let demoMap = new Map();
demoMap.set(1,'val1');
demoMap.set({},'val2');
console.log(demoMap.size);//2
Set
就是一個類似 Array
,但保證內容不重複的陣列。同時因其底層實作更貼近使用雜湊表(Hash Table)的方式,在新增,插入,刪除,查找的時間複雜度接近 O(1)(陣列是 O(n)),效能更好。arr[0]
)的方式,同時 Set
也不提供排序功能,需要有特定順序的集合時,還是得使用 array
。Set
和 array
做交互轉換的情況。這兩種資料結構都基於 ES 6 推出的迭代器規範來實作的。
接下來,讓我們看看實作迭代器規範是怎麼一回事。
如何遍歷一個可能含有多個值,多個屬性的資料結構,通常是一個資料結構的實作重點之一。
ES 6 的迭代器定義了一個介面,所有的迭代器物件都要符合這些規則和屬性。
迭代這個詞可能對比較沒看過的人有點拗口,迭代沒有上下文的時候指的是「交換替代」(Ref. 教育部國語辭典),用在數學和科學領域,這個詞一般指重複進行某個過程,且每次的操作都基於前一次的結果。
用資料集合的概念就是,每次做一樣的拿取行為,但資料結構自身會記憶訪問位置,每次訪問拿到移動後位置的值,且更新記憶的位置。透過不斷使用相同的拿取方法,可以依此方式不斷訪問取值直至迭代內容的終點。
來看看一個最基本的迭代器會長怎麼樣。(Code Source:MDN)
function makeRangeIterator(start = 0, end = Infinity, step = 1) {
let nextIndex = start;
let iterationCount = 0;
const rangeIterator = {
next() {
let result;
if (nextIndex < end) {
result = { value: nextIndex, done: false };
nextIndex += step;
iterationCount++;
return result;
}
return { value: iterationCount, done: true };
},
};
return rangeIterator;
}
//Use
const iter = makeRangeIterator(1, 10, 2);
let result = iter.next();
while (!result.done) {
console.log(result.value); // 1 3 5 7 9
result = iter.next();
}
console.log("Iterated over sequence of size:", result.value); // [5 numbers returned, that took interval in between: 0 to 10]
這是一個基本的迭代器,內部透過 nextIndex
記住了下一次 next()
方法訪問的對象(配合 step
定義如何更新指向位置)。
建構的時候通過 start
和 end
定義迭代器的資料範圍,當 next()
訪問超過 end
的時候就是到達迭代範圍的盡頭。
(順便提一下,nextIndex
和 iterationCount
即是透過閉包留存的,有注意到嗎?想了解更多關於閉包,請看 Day 12 的內容)
所以上面的例子最後印出來的結果會是 1 3 5 7 9
,因為建構時設置 step
為 2,起始為 1,終點為 10。每次 +2
,會在 9 停下。
一般來說,在實作迭代器的 next()
方法時,會回傳一個帶有兩個屬性的物件:{done:boolean, value: the value}
。
done
會回傳一個布林值,表示目前迭代器是否已到迭代範圍的終點,無法提供下一個值,value
則回傳這次呼叫 next()
時指向的值。
如果 done
為 true
,表示迭代已達終點,value
可不回傳或回傳 undefined
(無論哪種,訪問時都會回傳 undefined
)。
使用迭代器遍歷時,就是使用 next()
方法持續呼叫直到到達迭代範圍的終點。
有個詞叫做可迭代物件(Iterables),指的是該物件具有能被迭代方法 for of
訪問的方式,如 Array
或 Map
都有,但像 Object
就沒有。
要是一個可迭代物件,自身或原型鏈上的物件必須持有 Symbol.iterator
屬性,同時該屬性對應一個方法 [Symbol.iterator]()
,該方法需返回一個迭代器物件。
這段敘述可以看出可迭代物件和迭代器是不同的東西,迭代器更像是一種物件的實作規範,可迭代物件是另一種物件實作規範,其中可迭代物件規範了其一個屬性必須要回傳一個迭代器物件。
JS 中的常見可迭代物件包含 String
,Array
,Set
,Map
,Arguments
... 等等,還有下一篇會提到的 Generator
都是可迭代物件。
我們以陣列為例:
let arr = [1,2,3,4,5];
let iterator = arr[Symbol.iterator]();
console.log(iterator.toString());//[object Array Iterator]
console.log(iterator.next());//{done: false,value: 1}
console.log(iterator.next());//{done: false,value: 2}
可以看到例子中的 arr[Symbol.iterator]()
返回的 iterator
物件就是一個迭代器,而 arr
是一個可迭代物件。
相對陣列而言,迭代器是一個更高層的概念,透過迭代器的規範,我們能更好地去訪問實作了迭代器規範的物件內容。
想要特別提一下有一個和「可迭代」聽起來很像,但其實不一樣的觀念「可枚舉」。
枚舉一詞指的是逐一列出枚舉物件可枚舉的所有屬性或方法的過程。
針對物件的屬性或方法,有一個屬性 enumerable
可以被設定,原型鏈上的方法一般預設為 false
(大多討論枚舉的情況都是針對屬性,後面我會忽略方法,都簡稱為可枚舉屬性),一般情況下屬性則是預設為 true
。可枚舉指的是當進行枚舉行為時會列出來的東西,即 enumerable
為 true
的屬性。
class Human {
constructor( name ) {
this.name = name;
}
hello() {
console.log(`${this.name} says Hello`);
}
}
class Classmate extends Human {
constructor(name, studentId) {
super(name); //使用 Human 的建構方法建構
this.studentId = studentId;
}
showId() {
console.log(`My student Id is ${this.studentId}`);
}
}
let friend2 = new Classmate('Ryu','10000');
console.log(friend2.propertyIsEnumerable('name')); // true
console.log(friend2.propertyIsEnumerable('hello')); // false
console.log(friend2.propertyIsEnumerable('showId')); // false
for in
就是一個基於可枚舉實現的方法,當一個物件是可枚舉的,就可以使用 for in
來遍歷他的可枚舉屬性。
可枚舉的物件不一定可迭代,可迭代的物件也不一定可枚舉。
Object
let obj = {k1:'val1', k2:'val2'};
for (let key in obj) {
console.log(key);//k1, k2
}
for (let value of obj) {
console.log(value); //TypeError: obj is not iterable
}
Set
let set = new Set([1,2,3]);
for (let value of set) {
console.log(value); //1 2 3
}
for (let key in set) {//不會報錯,但也不會執行,不會印出任何值
console.log(key);
}
Array
let arr = ['a','b','c'];
for (let value of arr) {
console.log(value); //"a" "b" "c"
}
for (let key in arr) {
console.log(key); //0 1 2
}
通常討論可枚舉跟可迭代就是以 for in
和 for of
來舉例。
可以看到 for in
對應可枚舉,且以回傳索引為主,for of
則是回傳值、對應可迭代。
針對陣列的訪問,會更建議使用 for of
而非 for in
,因為 for in
會列出所有可枚舉對象,而 for of
則只會列出可迭代對象,更貼近我們一般遍歷陣列內容時希望的場景。
const arr = [10, 20, 30];
arr.foo = "bar";//非陣列索引的一環,附在陣列上的屬性
for (let key in arr) {
console.log(key); // "0" "1" "2" "foo"
}
for (let val of arr){
console.log(val);//10 20 30
}
一旦一個物件實作了迭代器規範,則表示該物件能夠使用下列函式 / 運算子。
for of
上面提到的,也是可迭代物件的定義。
需要能夠執行 for of
來遍歷才是一個可迭代物件。
展開運算子(...)
let a = [1, 2, 3];
let b = [0,...a]
console.log(b);//[0, 1, 2, 3]
解構運算子(let [a,b] = [1,2])
要順便提到的是除了可迭代物件能使用解構運算子之外,一般物件也能使用,但兩者的原理是不同的。
可迭代物件是基於他本身的可迭代性,依其順序進行迭代提值進行解構;物件則是針對鍵值對進行解構,解構出來的必須要對應鍵的名稱,與順序無關。
//可迭代的解構
let set = new Set([1, 2, 3, 4]);
let [first, second] = [...set];
console.log(first); // 1
console.log(second); // 2
//一般物件的鍵值對解構
let obj = {foo:'foo', bar:'bar'};
let {foo, bar} = obj;
console.log(foo);//"foo"
console.log(bar);//"bar"
Array.form()
用於將 類陣列物件(Array-Like) 或 可迭代物件(Iterables) 轉為陣列的靜態方法。
類陣列物件又與可枚舉,可迭代兩詞有出入,類陣列的先決條件是:
因為只是類陣列,類陣列也不保證能夠使用陣列的方法。總是是一個額外的類別定義。
總之類陣列物件或可迭代物件都可以使用這個方法來轉為陣列,轉換完便能夠使用陣列的那些方法,也具有陣列的那些屬性。
let set = new Set([3, 1, 2, 3, 3]);
console.log(set[0]);//undefined,Set 不是一個枚舉物件
let arr = Array.form(set);
console.log(arr[0]);//3,陣列是可枚舉物件,可透過索引訪問元素
console.log(arr);//[3, 1, 2]
有了迭代器的觀念,我們可以接著討論生成器(Generator)和相關的語法了。